/*
The CoSE layout was written by Gerardo Huck.
https://www.linkedin.com/in/gerardohuck/

Based on the following article:
http://dl.acm.org/citation.cfm?id=1498047

Modifications tracked on Github.
*/

import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';

var DEBUG;

/**
 * @brief :  default layout options
 */
var defaults = {
  // Called on `layoutready`
  ready: function(){},

  // Called on `layoutstop`
  stop: function(){},

  // Whether to animate while running the layout
  // true : Animate continuously as the layout is running
  // false : Just show the end result
  // 'end' : Animate with the end result, from the initial positions to the end positions
  animate: true,

  // Easing of the animation for animate:'end'
  animationEasing: undefined,

  // The duration of the animation for animate:'end'
  animationDuration: undefined,

  // A function that determines whether the node should be animated
  // All nodes animated by default on animate enabled
  // Non-animated nodes are positioned immediately when the layout starts
  animateFilter: function ( node, i ){ return true; },


  // The layout animates only after this many milliseconds for animate:true
  // (prevents flashing on fast runs)
  animationThreshold: 250,

  // Number of iterations between consecutive screen positions update
  refresh: 20,

  // Whether to fit the network view after when done
  fit: true,

  // Padding on fit
  padding: 30,

  // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
  boundingBox: undefined,

  // Excludes the label when calculating node bounding boxes for the layout algorithm
  nodeDimensionsIncludeLabels: false,

  // Randomize the initial positions of the nodes (true) or use existing positions (false)
  randomize: false,

  // Extra spacing between components in non-compound graphs
  componentSpacing: 40,

  // Node repulsion (non overlapping) multiplier
  nodeRepulsion: function( node ){ return 2048; },

  // Node repulsion (overlapping) multiplier
  nodeOverlap: 4,

  // Ideal edge (non nested) length
  idealEdgeLength: function( edge ){ return 32; },

  // Divisor to compute edge forces
  edgeElasticity: function( edge ){ return 32; },

  // Nesting factor (multiplier) to compute ideal edge length for nested edges
  nestingFactor: 1.2,

  // Gravity force (constant)
  gravity: 1,

  // Maximum number of iterations to perform
  numIter: 1000,

  // Initial temperature (maximum node displacement)
  initialTemp: 1000,

  // Cooling factor (how the temperature is reduced between consecutive iterations
  coolingFactor: 0.99,

  // Lower temperature threshold (below this point the layout will end)
  minTemp: 1.0
};


/**
 * @brief       : constructor
 * @arg options : object containing layout options
 */
function CoseLayout( options ){
  this.options = util.extend( {}, defaults, options );
  this.options.layout = this;

  // Exclude any edge that has a source or target node that is not in the set of passed-in nodes
  const nodes = this.options.eles.nodes();
  const edges = this.options.eles.edges();
  const notEdges = edges.filter((e) => {
    const sourceId = e.source().data('id');
    const targetId = e.target().data('id');
    const hasSource = nodes.some((n) => n.data('id') === sourceId);
    const hasTarget = nodes.some((n) => n.data('id') === targetId);
    return !hasSource || !hasTarget;
  });
  this.options.eles = this.options.eles.not(notEdges);
}

/**
 * @brief : runs the layout
 */
CoseLayout.prototype.run = function(){
  var options = this.options;
  var cy      = options.cy;
  var layout  = this;

  layout.stopped = false;

  if( options.animate === true || options.animate === false ){
    layout.emit( { type: 'layoutstart', layout: layout } );
  }

  // Set DEBUG - Global variable
  if( true === options.debug ){
    DEBUG = true;
  } else {
    DEBUG = false;
  }

  // Initialize layout info
  var layoutInfo = createLayoutInfo( cy, layout, options );

  // Show LayoutInfo contents if debugging
  if( DEBUG ){
    printLayoutInfo( layoutInfo );
  }

  // If required, randomize node positions
  if (options.randomize) {
    randomizePositions( layoutInfo, cy );
  }

  var startTime = util.performanceNow();

  var refresh = function(){
    refreshPositions( layoutInfo, cy, options );

    // Fit the graph if necessary
    if( true === options.fit ){
      cy.fit( options.padding );
    }
  };

  var mainLoop = function( i ){
    if( layout.stopped || i >= options.numIter ){
      // logDebug("Layout manually stopped. Stopping computation in step " + i);
      return false;
    }

    // Do one step in the phisical simulation
    step( layoutInfo, options, i );

    // Update temperature
    layoutInfo.temperature = layoutInfo.temperature * options.coolingFactor;
    // logDebug("New temperature: " + layoutInfo.temperature);

    if( layoutInfo.temperature < options.minTemp ){
      // logDebug("Temperature drop below minimum threshold. Stopping computation in step " + i);
      return false;
    }

    return true;
  };

  var done = function(){
    if( options.animate === true || options.animate === false ){
      refresh();

      // Layout has finished
      layout.one('layoutstop', options.stop);
      layout.emit({ type: 'layoutstop', layout: layout });
    } else {
      var nodes = options.eles.nodes();
      var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes);

      nodes.layoutPositions(layout, options, getScaledPos);
    }
  };

  var i = 0;
  var loopRet = true;

  if( options.animate === true ){
    var frame = function(){
      var f = 0;

      while( loopRet && f < options.refresh ){
        loopRet = mainLoop(i);

        i++;
        f++;
      }

      if( !loopRet ){ // it's done
        separateComponents( layoutInfo, options );
        done();
      } else {
        var now = util.performanceNow();

        if( now - startTime >= options.animationThreshold ){
          refresh();
        }

        util.requestAnimationFrame(frame);
      }
    };

    frame();
  } else {
    while( loopRet ){
      loopRet = mainLoop(i);

      i++;
    }

    separateComponents( layoutInfo, options );
    done();
  }

  return this; // chaining
};


/**
 * @brief : called on continuous layouts to stop them before they finish
 */
CoseLayout.prototype.stop = function(){
  this.stopped = true;

  if( this.thread ){
    this.thread.stop();
  }

  this.emit( 'layoutstop' );

  return this; // chaining
};

CoseLayout.prototype.destroy = function(){
  if( this.thread ){
    this.thread.stop();
  }

  return this; // chaining
};


/**
 * @brief     : Creates an object which is contains all the data
 *              used in the layout process
 * @arg cy    : cytoscape.js object
 * @return    : layoutInfo object initialized
 */
var createLayoutInfo = function( cy, layout, options ){
  // Shortcut
  var edges = options.eles.edges();
  var nodes = options.eles.nodes();
  var bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
    x1: 0, y1: 0, w: cy.width(), h: cy.height()
  } );

  var layoutInfo   = {
    isCompound: cy.hasCompoundNodes(),
    layoutNodes: [],
    idToIndex: {},
    nodeSize: nodes.size(),
    graphSet: [],
    indexToGraph: [],
    layoutEdges: [],
    edgeSize: edges.size(),
    temperature: options.initialTemp,
    clientWidth: bb.w,
    clientHeight: bb.h,
    boundingBox: bb
  };

  var components = options.eles.components();
  var id2cmptId = {};

  for( var i = 0; i < components.length; i++ ){
    var component = components[ i ];

    for( var j = 0; j < component.length; j++ ){
      var node = component[ j ];

      id2cmptId[ node.id() ] = i;
    }
  }

  // Iterate over all nodes, creating layout nodes
  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = nodes[ i ];
    var nbb = n.layoutDimensions( options );

    var tempNode        = {};
    tempNode.isLocked   = n.locked();
    tempNode.id         = n.data( 'id' );
    tempNode.parentId   = n.data( 'parent' );
    tempNode.cmptId     = id2cmptId[ n.id() ];
    tempNode.children   = [];
    tempNode.positionX  = n.position( 'x' );
    tempNode.positionY  = n.position( 'y' );
    tempNode.offsetX    = 0;
    tempNode.offsetY    = 0;
    tempNode.height     = nbb.w;
    tempNode.width      = nbb.h;
    tempNode.maxX       = tempNode.positionX + tempNode.width  / 2;
    tempNode.minX       = tempNode.positionX - tempNode.width  / 2;
    tempNode.maxY       = tempNode.positionY + tempNode.height / 2;
    tempNode.minY       = tempNode.positionY - tempNode.height / 2;
    tempNode.padLeft    = parseFloat( n.style( 'padding' ) );
    tempNode.padRight   = parseFloat( n.style( 'padding' ) );
    tempNode.padTop     = parseFloat( n.style( 'padding' ) );
    tempNode.padBottom  = parseFloat( n.style( 'padding' ) );

    // forces
    tempNode.nodeRepulsion = is.fn( options.nodeRepulsion ) ? options.nodeRepulsion(n) : options.nodeRepulsion;

    // Add new node
    layoutInfo.layoutNodes.push( tempNode );
    // Add entry to id-index map
    layoutInfo.idToIndex[ tempNode.id ] = i;
  }

  // Inline implementation of a queue, used for traversing the graph in BFS order
  var queue = [];
  var start = 0;   // Points to the start the queue
  var end   = -1;  // Points to the end of the queue

  var tempGraph = [];

  // Second pass to add child information and
  // initialize queue for hierarchical traversal
  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = layoutInfo.layoutNodes[ i ];
    var p_id = n.parentId;
    // Check if node n has a parent node
    if( null != p_id ){
      // Add node Id to parent's list of children
      layoutInfo.layoutNodes[ layoutInfo.idToIndex[ p_id ] ].children.push( n.id );
    } else {
      // If a node doesn't have a parent, then it's in the root graph
      queue[ ++end ] = n.id;
      tempGraph.push( n.id );
    }
  }

  // Add root graph to graphSet
  layoutInfo.graphSet.push( tempGraph );

  // Traverse the graph, level by level,
  while( start <= end ){
    // Get the node to visit and remove it from queue
    var node_id  = queue[ start++ ];
    var node_ix  = layoutInfo.idToIndex[ node_id ];
    var node     = layoutInfo.layoutNodes[ node_ix ];
    var children = node.children;
    if( children.length > 0 ){
      // Add children nodes as a new graph to graph set
      layoutInfo.graphSet.push( children );
      // Add children to que queue to be visited
      for( var i = 0; i < children.length; i++ ){
        queue[ ++end ] = children[ i ];
      }
    }
  }

  // Create indexToGraph map
  for( var i = 0; i < layoutInfo.graphSet.length; i++ ){
    var graph = layoutInfo.graphSet[ i ];
    for( var j = 0; j < graph.length; j++ ){
      var index = layoutInfo.idToIndex[ graph[ j ] ];
      layoutInfo.indexToGraph[ index ] = i;
    }
  }

  // Iterate over all edges, creating Layout Edges
  for( var i = 0; i < layoutInfo.edgeSize; i++ ){
    var e = edges[ i ];
    var tempEdge = {};
    tempEdge.id       = e.data( 'id' );
    tempEdge.sourceId = e.data( 'source' );
    tempEdge.targetId = e.data( 'target' );

    // Compute ideal length
    var idealLength = is.fn( options.idealEdgeLength ) ? options.idealEdgeLength(e) : options.idealEdgeLength;
    var elasticity = is.fn( options.edgeElasticity ) ? options.edgeElasticity(e) : options.edgeElasticity;

    // Check if it's an inter graph edge
    var sourceIx    = layoutInfo.idToIndex[ tempEdge.sourceId ];
    var targetIx    = layoutInfo.idToIndex[ tempEdge.targetId ];
    var sourceGraph = layoutInfo.indexToGraph[ sourceIx ];
    var targetGraph = layoutInfo.indexToGraph[ targetIx ];

    if( sourceGraph != targetGraph ){
      // Find lowest common graph ancestor
      var lca = findLCA( tempEdge.sourceId, tempEdge.targetId, layoutInfo );

      // Compute sum of node depths, relative to lca graph
      var lcaGraph = layoutInfo.graphSet[ lca ];
      var depth    = 0;

      // Source depth
      var tempNode = layoutInfo.layoutNodes[ sourceIx ];
      while( -1 === lcaGraph.indexOf( tempNode.id ) ){
        tempNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ tempNode.parentId ] ];
        depth++;
      }

      // Target depth
      tempNode = layoutInfo.layoutNodes[ targetIx ];
      while( -1 === lcaGraph.indexOf( tempNode.id ) ){
        tempNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ tempNode.parentId ] ];
        depth++;
      }

      // logDebug('LCA of nodes ' + tempEdge.sourceId + ' and ' + tempEdge.targetId +
      //  ". Index: " + lca + " Contents: " + lcaGraph.toString() +
      //  ". Depth: " + depth);

      // Update idealLength
      idealLength *= depth * options.nestingFactor;
    }

    tempEdge.idealLength = idealLength;
    tempEdge.elasticity = elasticity;

    layoutInfo.layoutEdges.push( tempEdge );
  }

  // Finally, return layoutInfo object
  return layoutInfo;
};


/**
 * @brief : This function finds the index of the lowest common
 *          graph ancestor between 2 nodes in the subtree
 *          (from the graph hierarchy induced tree) whose
 *          root is graphIx
 *
 * @arg node1: node1's ID
 * @arg node2: node2's ID
 * @arg layoutInfo: layoutInfo object
 *
 */
var findLCA = function( node1, node2, layoutInfo ){
  // Find their common ancester, starting from the root graph
  var res = findLCA_aux( node1, node2, 0, layoutInfo );
  if( 2 > res.count ){
    // If aux function couldn't find the common ancester,
    // then it is the root graph
    return 0;
  } else {
    return res.graph;
  }
};


/**
 * @brief          : Auxiliary function used for LCA computation
 *
 * @arg node1      : node1's ID
 * @arg node2      : node2's ID
 * @arg graphIx    : subgraph index
 * @arg layoutInfo : layoutInfo object
 *
 * @return         : object of the form {count: X, graph: Y}, where:
 *                   X is the number of ancestors (max: 2) found in
 *                   graphIx (and it's subgraphs),
 *                   Y is the graph index of the lowest graph containing
 *                   all X nodes
 */
var findLCA_aux = function( node1, node2, graphIx, layoutInfo ){
  var graph = layoutInfo.graphSet[ graphIx ];
  // If both nodes belongs to graphIx
  if( -1 < graph.indexOf( node1 ) && -1 < graph.indexOf( node2 ) ){
    return {count: 2, graph: graphIx};
  }

  // Make recursive calls for all subgraphs
  var c = 0;
  for( var i = 0; i < graph.length; i++ ){
    var nodeId   = graph[ i ];
    var nodeIx   = layoutInfo.idToIndex[ nodeId ];
    var children = layoutInfo.layoutNodes[ nodeIx ].children;

    // If the node has no child, skip it
    if( 0 === children.length ){
      continue;
    }

    var childGraphIx = layoutInfo.indexToGraph[ layoutInfo.idToIndex[ children[0] ] ];
    var result = findLCA_aux( node1, node2, childGraphIx, layoutInfo );
    if( 0 === result.count ){
      // Neither node1 nor node2 are present in this subgraph
      continue;
    } else if( 1 === result.count ){
      // One of (node1, node2) is present in this subgraph
      c++;
      if( 2 === c ){
        // We've already found both nodes, no need to keep searching
        break;
      }
    } else {
      // Both nodes are present in this subgraph
      return result;
    }
  }

  return {count: c, graph: graphIx};
};


/**
 * @brief: printsLayoutInfo into js console
 *         Only used for debbuging
 */
if( process.env.NODE_ENV !== 'production' ){
  var printLayoutInfo = function( layoutInfo ){
    /* eslint-disable */

    if( !DEBUG ){
      return;
    }
    console.debug( 'layoutNodes:' );
    for( var i = 0; i < layoutInfo.nodeSize; i++ ){
      var n = layoutInfo.layoutNodes[ i ];
      var s =
      '\nindex: '     + i +
      '\nId: '        + n.id +
      '\nChildren: '  + n.children.toString() +
      '\nparentId: '  + n.parentId  +
      '\npositionX: ' + n.positionX +
      '\npositionY: ' + n.positionY +
      '\nOffsetX: ' + n.offsetX +
      '\nOffsetY: ' + n.offsetY +
      '\npadLeft: ' + n.padLeft +
      '\npadRight: ' + n.padRight +
      '\npadTop: ' + n.padTop +
      '\npadBottom: ' + n.padBottom;

      console.debug( s );
    }

    console.debug( 'idToIndex' );
    for( var i in layoutInfo.idToIndex ){
      console.debug( 'Id: ' + i + '\nIndex: ' + layoutInfo.idToIndex[ i ] );
    }

    console.debug( 'Graph Set' );
    var set = layoutInfo.graphSet;
    for( var i = 0; i < set.length; i ++ ){
      console.debug( 'Set : ' + i + ': ' + set[ i ].toString() );
    }

    var s = 'IndexToGraph';
    for( var i = 0; i < layoutInfo.indexToGraph.length; i ++ ){
      s += '\nIndex : ' + i + ' Graph: ' + layoutInfo.indexToGraph[ i ];
    }
    console.debug( s );

    s = 'Layout Edges';
    for( var i = 0; i < layoutInfo.layoutEdges.length; i++ ){
      var e = layoutInfo.layoutEdges[ i ];
      s += '\nEdge Index: ' + i + ' ID: ' + e.id +
      ' SouceID: ' + e.sourceId + ' TargetId: ' + e.targetId +
      ' Ideal Length: ' + e.idealLength;
    }
    console.debug( s );

    s =  'nodeSize: ' + layoutInfo.nodeSize;
    s += '\nedgeSize: ' + layoutInfo.edgeSize;
    s += '\ntemperature: ' + layoutInfo.temperature;
    console.debug( s );

    return;
    /* eslint-enable */
  };
}

/**
 * @brief : Randomizes the position of all nodes
 */
var randomizePositions = function( layoutInfo, cy ){
  var width     = layoutInfo.clientWidth;
  var height    = layoutInfo.clientHeight;

  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = layoutInfo.layoutNodes[ i ];

    // No need to randomize compound nodes or locked nodes
    if( 0 === n.children.length && !n.isLocked ){
      n.positionX = Math.random() * width;
      n.positionY = Math.random() * height;
    }
  }
};

var getScaleInBoundsFn = function( layoutInfo, options, nodes ){
  var bb = layoutInfo.boundingBox;
  var coseBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };

  if( options.boundingBox ){
    nodes.forEach( function( node ){
      var lnode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ node.data( 'id' ) ] ];

      coseBB.x1 = Math.min( coseBB.x1, lnode.positionX );
      coseBB.x2 = Math.max( coseBB.x2, lnode.positionX );

      coseBB.y1 = Math.min( coseBB.y1, lnode.positionY );
      coseBB.y2 = Math.max( coseBB.y2, lnode.positionY );
    } );

    coseBB.w = coseBB.x2 - coseBB.x1;
    coseBB.h = coseBB.y2 - coseBB.y1;
  }

  return function( ele, i ){
    var lnode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ ele.data( 'id' ) ] ];

    if( options.boundingBox ){ // then add extra bounding box constraint
      var pctX = (lnode.positionX - coseBB.x1) / coseBB.w;
      var pctY = (lnode.positionY - coseBB.y1) / coseBB.h;

      return {
        x: bb.x1 + pctX * bb.w,
        y: bb.y1 + pctY * bb.h
      };
    } else {
      return {
        x: lnode.positionX,
        y: lnode.positionY
      };
    }
  };
};

/**
 * @brief          : Updates the positions of nodes in the network
 * @arg layoutInfo : LayoutInfo object
 * @arg cy         : Cytoscape object
 * @arg options    : Layout options
 */
var refreshPositions = function( layoutInfo, cy, options ){
  // var s = 'Refreshing positions';
  // logDebug(s);

  var layout = options.layout;
  var nodes = options.eles.nodes();
  var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes);

  nodes.positions(getScaledPos);

  // Trigger layoutReady only on first call
  if( true !== layoutInfo.ready ){
    // s = 'Triggering layoutready';
    // logDebug(s);
    layoutInfo.ready = true;
    layout.one( 'layoutready', options.ready );
    layout.emit( { type: 'layoutready', layout: this } );
  }
};

/**
 * @brief : Logs a debug message in JS console, if DEBUG is ON
 */
// var logDebug = function(text) {
//   if (DEBUG) {
//     console.debug(text);
//   }
// };

/**
 * @brief          : Performs one iteration of the physical simulation
 * @arg layoutInfo : LayoutInfo object already initialized
 * @arg cy         : Cytoscape object
 * @arg options    : Layout options
 */
var step = function( layoutInfo, options, step ){
  // var s = "\n\n###############################";
  // s += "\nSTEP: " + step;
  // s += "\n###############################\n";
  // logDebug(s);

  // Calculate node repulsions
  calculateNodeForces( layoutInfo, options );
  // Calculate edge forces
  calculateEdgeForces( layoutInfo, options );
  // Calculate gravity forces
  calculateGravityForces( layoutInfo, options );
  // Propagate forces from parent to child
  propagateForces( layoutInfo, options );
  // Update positions based on calculated forces
  updatePositions( layoutInfo, options );
};

/**
 * @brief : Computes the node repulsion forces
 */
var calculateNodeForces = function( layoutInfo, options ){
  // Go through each of the graphs in graphSet
  // Nodes only repel each other if they belong to the same graph
  // var s = 'calculateNodeForces';
  // logDebug(s);
  for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){
    var graph    = layoutInfo.graphSet[ i ];
    var numNodes = graph.length;

    // s = "Set: " + graph.toString();
    // logDebug(s);

    // Now get all the pairs of nodes
    // Only get each pair once, (A, B) = (B, A)
    for( var j = 0; j < numNodes; j++ ){
      var node1 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ];

      for( var k = j + 1; k < numNodes; k++ ){
        var node2 = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ k ] ] ];

        nodeRepulsion( node1, node2, layoutInfo, options );
      }
    }
  }
};

var randomDistance = function( max ){
  return -max + 2 * max * Math.random();
};

/**
 * @brief : Compute the node repulsion forces between a pair of nodes
 */
var nodeRepulsion = function( node1, node2, layoutInfo, options ){
  // var s = "Node repulsion. Node1: " + node1.id + " Node2: " + node2.id;

  var cmptId1 = node1.cmptId;
  var cmptId2 = node2.cmptId;

  if( cmptId1 !== cmptId2 && !layoutInfo.isCompound ){ return; }

  // Get direction of line connecting both node centers
  var directionX = node2.positionX - node1.positionX;
  var directionY = node2.positionY - node1.positionY;
  var maxRandDist = 1;
  // s += "\ndirectionX: " + directionX + ", directionY: " + directionY;

  // If both centers are the same, apply a random force
  if( 0 === directionX && 0 === directionY ){
    directionX = randomDistance( maxRandDist );
    directionY = randomDistance( maxRandDist );
  }

  var overlap = nodesOverlap( node1, node2, directionX, directionY );

  if( overlap > 0 ){
    // s += "\nNodes DO overlap.";
    // s += "\nOverlap: " + overlap;
    // If nodes overlap, repulsion force is proportional
    // to the overlap
    var force    = options.nodeOverlap * overlap;

    // Compute the module and components of the force vector
    var distance = Math.sqrt( directionX * directionX + directionY * directionY );
    // s += "\nDistance: " + distance;
    var forceX   = force * directionX / distance;
    var forceY   = force * directionY / distance;

  } else {
    // s += "\nNodes do NOT overlap.";
    // If there's no overlap, force is inversely proportional
    // to squared distance

    // Get clipping points for both nodes
    var point1 = findClippingPoint( node1, directionX, directionY );
    var point2 = findClippingPoint( node2, -1 * directionX, -1 * directionY );

    // Use clipping points to compute distance
    var distanceX   = point2.x - point1.x;
    var distanceY   = point2.y - point1.y;
    var distanceSqr = distanceX * distanceX + distanceY * distanceY;
    var distance    = Math.sqrt( distanceSqr );
    // s += "\nDistance: " + distance;

    // Compute the module and components of the force vector
    var force  = ( node1.nodeRepulsion + node2.nodeRepulsion ) / distanceSqr;
    var forceX = force * distanceX / distance;
    var forceY = force * distanceY / distance;
  }

  // Apply force
  if( !node1.isLocked ){
    node1.offsetX -= forceX;
    node1.offsetY -= forceY;
  }

  if( !node2.isLocked ){
    node2.offsetX += forceX;
    node2.offsetY += forceY;
  }

  // s += "\nForceX: " + forceX + " ForceY: " + forceY;
  // logDebug(s);

  return;
};

/**
 * @brief  : Determines whether two nodes overlap or not
 * @return : Amount of overlapping (0 => no overlap)
 */
var nodesOverlap = function( node1, node2, dX, dY ){

  if( dX > 0 ){
    var overlapX = node1.maxX - node2.minX;
  } else {
    var overlapX = node2.maxX - node1.minX;
  }

  if( dY > 0 ){
    var overlapY = node1.maxY - node2.minY;
  } else {
    var overlapY = node2.maxY - node1.minY;
  }

  if( overlapX >= 0 && overlapY >= 0 ){
    return Math.sqrt( overlapX * overlapX + overlapY * overlapY );
  } else {
    return 0;
  }
};

/**
 * @brief : Finds the point in which an edge (direction dX, dY) intersects
 *          the rectangular bounding box of it's source/target node
 */
var findClippingPoint = function( node, dX, dY ){

  // Shorcuts
  var X = node.positionX;
  var Y = node.positionY;
  var H = node.height || 1;
  var W = node.width || 1;
  var dirSlope     = dY / dX;
  var nodeSlope    = H / W;

  // var s = 'Computing clipping point of node ' + node.id +
  //   " . Height:  " + H + ", Width: " + W +
  //   "\nDirection " + dX + ", " + dY;
  //
  // Compute intersection
  var res = {};

  // Case: Vertical direction (up)
  if( 0 === dX && 0 < dY ){
    res.x = X;
    // s += "\nUp direction";
    res.y = Y + H / 2;

    return res;
  }

  // Case: Vertical direction (down)
  if( 0 === dX && 0 > dY ){
    res.x = X;
    res.y = Y + H / 2;
    // s += "\nDown direction";

    return res;
  }

  // Case: Intersects the right border
  if( 0 < dX &&
  -1 * nodeSlope <= dirSlope &&
  dirSlope <= nodeSlope ){
    res.x = X + W / 2;
    res.y = Y + (W * dY / 2 / dX);
    // s += "\nRightborder";

    return res;
  }

  // Case: Intersects the left border
  if( 0 > dX &&
  -1 * nodeSlope <= dirSlope &&
  dirSlope <= nodeSlope ){
    res.x = X - W / 2;
    res.y = Y - (W * dY / 2 / dX);
    // s += "\nLeftborder";

    return res;
  }

  // Case: Intersects the top border
  if( 0 < dY &&
  ( dirSlope <= -1 * nodeSlope ||
    dirSlope >= nodeSlope ) ){
    res.x = X + (H * dX / 2 / dY);
    res.y = Y + H / 2;
    // s += "\nTop border";

    return res;
  }

  // Case: Intersects the bottom border
  if( 0 > dY &&
  ( dirSlope <= -1 * nodeSlope ||
    dirSlope >= nodeSlope ) ){
    res.x = X - (H * dX / 2 / dY);
    res.y = Y - H / 2;
    // s += "\nBottom border";

    return res;
  }

  // s += "\nClipping point found at " + res.x + ", " + res.y;
  // logDebug(s);
  return res;
};

/**
 * @brief : Calculates all edge forces
 */
var calculateEdgeForces = function( layoutInfo, options ){
  // Iterate over all edges
  for( var i = 0; i < layoutInfo.edgeSize; i++ ){
    // Get edge, source & target nodes
    var edge     = layoutInfo.layoutEdges[ i ];
    var sourceIx = layoutInfo.idToIndex[ edge.sourceId ];
    var source   = layoutInfo.layoutNodes[ sourceIx ];
    var targetIx = layoutInfo.idToIndex[ edge.targetId ];
    var target   = layoutInfo.layoutNodes[ targetIx ];

    // Get direction of line connecting both node centers
    var directionX = target.positionX - source.positionX;
    var directionY = target.positionY - source.positionY;

    // If both centers are the same, do nothing.
    // A random force has already been applied as node repulsion
    if( 0 === directionX && 0 === directionY ){
      continue;
    }

    // Get clipping points for both nodes
    var point1 = findClippingPoint( source, directionX, directionY );
    var point2 = findClippingPoint( target, -1 * directionX, -1 * directionY );


    var lx = point2.x - point1.x;
    var ly = point2.y - point1.y;
    var l  = Math.sqrt( lx * lx + ly * ly );

    var force  = Math.pow( edge.idealLength - l, 2 ) / edge.elasticity;

    if( 0 !== l ){
      var forceX = force * lx / l;
      var forceY = force * ly / l;
    } else {
      var forceX = 0;
      var forceY = 0;
    }

    // Add this force to target and source nodes
    if( !source.isLocked ){
      source.offsetX += forceX;
      source.offsetY += forceY;
    }

    if( !target.isLocked ){
      target.offsetX -= forceX;
      target.offsetY -= forceY;
    }

    // var s = 'Edge force between nodes ' + source.id + ' and ' + target.id;
    // s += "\nDistance: " + l + " Force: (" + forceX + ", " + forceY + ")";
    // logDebug(s);
  }
};

/**
 * @brief : Computes gravity forces for all nodes
 */
var calculateGravityForces = function( layoutInfo, options ){
  if (options.gravity === 0) {
    return;
  }

  var distThreshold = 1;

  // var s = 'calculateGravityForces';
  // logDebug(s);
  for( var i = 0; i < layoutInfo.graphSet.length; i ++ ){
    var graph    = layoutInfo.graphSet[ i ];
    var numNodes = graph.length;

    // s = "Set: " + graph.toString();
    // logDebug(s);

    // Compute graph center
    if( 0 === i ){
      var centerX   = layoutInfo.clientHeight / 2;
      var centerY   = layoutInfo.clientWidth  / 2;
    } else {
      // Get Parent node for this graph, and use its position as center
      var temp    = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[0] ] ];
      var parent  = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ temp.parentId ] ];
      var centerX = parent.positionX;
      var centerY = parent.positionY;
    }
    // s = "Center found at: " + centerX + ", " + centerY;
    // logDebug(s);

    // Apply force to all nodes in graph
    for( var j = 0; j < numNodes; j++ ){
      var node = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ graph[ j ] ] ];
      // s = "Node: " + node.id;

      if( node.isLocked ){ continue; }

      var dx = centerX - node.positionX;
      var dy = centerY - node.positionY;
      var d  = Math.sqrt( dx * dx + dy * dy );
      if( d > distThreshold ){
        var fx = options.gravity * dx / d;
        var fy = options.gravity * dy / d;
        node.offsetX += fx;
        node.offsetY += fy;
        // s += ": Applied force: " + fx + ", " + fy;
      } else {
        // s += ": skypped since it's too close to center";
      }
      // logDebug(s);
    }
  }
};

/**
 * @brief          : This function propagates the existing offsets from
 *                   parent nodes to its descendents.
 * @arg layoutInfo : layoutInfo Object
 * @arg cy         : cytoscape Object
 * @arg options    : Layout options
 */
var propagateForces = function( layoutInfo, options ){
  // Inline implementation of a queue, used for traversing the graph in BFS order
  var queue = [];
  var start = 0;   // Points to the start the queue
  var end   = -1;  // Points to the end of the queue

  // logDebug('propagateForces');

  // Start by visiting the nodes in the root graph
  queue.push.apply( queue, layoutInfo.graphSet[0] );
  end += layoutInfo.graphSet[0].length;

  // Traverse the graph, level by level,
  while( start <= end ){
    // Get the node to visit and remove it from queue
    var nodeId    = queue[ start++ ];
    var nodeIndex = layoutInfo.idToIndex[ nodeId ];
    var node      = layoutInfo.layoutNodes[ nodeIndex ];
    var children  = node.children;

    // We only need to process the node if it's compound
    if( 0 < children.length && !node.isLocked ){
      var offX = node.offsetX;
      var offY = node.offsetY;

      // var s = "Propagating offset from parent node : " + node.id +
      //   ". OffsetX: " + offX + ". OffsetY: " + offY;
      // s += "\n Children: " + children.toString();
      // logDebug(s);

      for( var i = 0; i < children.length; i++ ){
        var childNode = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ children[ i ] ] ];
        // Propagate offset
        childNode.offsetX += offX;
        childNode.offsetY += offY;
        // Add children to queue to be visited
        queue[ ++end ] = children[ i ];
      }

      // Reset parent offsets
      node.offsetX = 0;
      node.offsetY = 0;
    }

  }
};

/**
 * @brief : Updates the layout model positions, based on
 *          the accumulated forces
 */
var updatePositions = function( layoutInfo, options ){
  // var s = 'Updating positions';
  // logDebug(s);

  // Reset boundaries for compound nodes
  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = layoutInfo.layoutNodes[ i ];
    if( 0 < n.children.length ){
      // logDebug("Resetting boundaries of compound node: " + n.id);
      n.maxX = undefined;
      n.minX = undefined;
      n.maxY = undefined;
      n.minY = undefined;
    }
  }

  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = layoutInfo.layoutNodes[ i ];
    if( 0 < n.children.length || n.isLocked ){
      // No need to set compound or locked node position
      // logDebug("Skipping position update of node: " + n.id);
      continue;
    }
    // s = "Node: " + n.id + " Previous position: (" +
    // n.positionX + ", " + n.positionY + ").";

    // Limit displacement in order to improve stability
    var tempForce = limitForce( n.offsetX, n.offsetY, layoutInfo.temperature );
    n.positionX += tempForce.x;
    n.positionY += tempForce.y;
    n.offsetX = 0;
    n.offsetY = 0;
    n.minX    = n.positionX - n.width;
    n.maxX    = n.positionX + n.width;
    n.minY    = n.positionY - n.height;
    n.maxY    = n.positionY + n.height;
    // s += " New Position: (" + n.positionX + ", " + n.positionY + ").";
    // logDebug(s);

    // Update ancestry boudaries
    updateAncestryBoundaries( n, layoutInfo );
  }

  // Update size, position of compund nodes
  for( var i = 0; i < layoutInfo.nodeSize; i++ ){
    var n = layoutInfo.layoutNodes[ i ];
    if( 0 < n.children.length && !n.isLocked ){
      n.positionX = (n.maxX + n.minX) / 2;
      n.positionY = (n.maxY + n.minY) / 2;
      n.width     = n.maxX - n.minX;
      n.height    = n.maxY - n.minY;
      // s = "Updating position, size of compound node " + n.id;
      // s += "\nPositionX: " + n.positionX + ", PositionY: " + n.positionY;
      // s += "\nWidth: " + n.width + ", Height: " + n.height;
      // logDebug(s);
    }
  }
};

/**
 * @brief : Limits a force (forceX, forceY) to be not
 *          greater (in modulo) than max.
 8          Preserves force direction.
  */
var limitForce = function( forceX, forceY, max ){
  // var s = "Limiting force: (" + forceX + ", " + forceY + "). Max: " + max;
  var force = Math.sqrt( forceX * forceX + forceY * forceY );

  if( force > max ){
    var res = {
      x: max * forceX / force,
      y: max * forceY / force
    };

  } else {
    var res = {
      x: forceX,
      y: forceY
    };
  }

  // s += ".\nResult: (" + res.x + ", " + res.y + ")";
  // logDebug(s);

  return res;
};

/**
 * @brief : Function used for keeping track of compound node
 *          sizes, since they should bound all their subnodes.
 */
var updateAncestryBoundaries = function( node, layoutInfo ){
  // var s = "Propagating new position/size of node " + node.id;
  var parentId = node.parentId;
  if( null == parentId ){
    // If there's no parent, we are done
    // s += ". No parent node.";
    // logDebug(s);
    return;
  }

  // Get Parent Node
  var p = layoutInfo.layoutNodes[ layoutInfo.idToIndex[ parentId ] ];
  var flag = false;

  // MaxX
  if( null == p.maxX || node.maxX + p.padRight > p.maxX ){
    p.maxX = node.maxX + p.padRight;
    flag = true;
    // s += "\nNew maxX for parent node " + p.id + ": " + p.maxX;
  }

  // MinX
  if( null == p.minX || node.minX - p.padLeft < p.minX ){
    p.minX = node.minX - p.padLeft;
    flag = true;
    // s += "\nNew minX for parent node " + p.id + ": " + p.minX;
  }

  // MaxY
  if( null == p.maxY || node.maxY + p.padBottom > p.maxY ){
    p.maxY = node.maxY + p.padBottom;
    flag = true;
    // s += "\nNew maxY for parent node " + p.id + ": " + p.maxY;
  }

  // MinY
  if( null == p.minY || node.minY - p.padTop < p.minY ){
    p.minY = node.minY - p.padTop;
    flag = true;
    // s += "\nNew minY for parent node " + p.id + ": " + p.minY;
  }

  // If updated boundaries, propagate changes upward
  if( flag ){
    // logDebug(s);
    return updateAncestryBoundaries( p, layoutInfo );
  }

  // s += ". No changes in boundaries/position of parent node " + p.id;
  // logDebug(s);
  return;
};

var separateComponents = function( layoutInfo, options ){
  var nodes = layoutInfo.layoutNodes;
  var components = [];

  for( var i = 0; i < nodes.length; i++ ){
    var node = nodes[ i ];
    var cid = node.cmptId;
    var component = components[ cid ] = components[ cid ] || [];

    component.push( node );
  }

  var totalA = 0;

  for( var i = 0; i < components.length; i++ ){
    var c = components[ i ];

    if( !c ){ continue; }

    c.x1 = Infinity;
    c.x2 = -Infinity;
    c.y1 = Infinity;
    c.y2 = -Infinity;

    for( var j = 0; j < c.length; j++ ){
      var n = c[ j ];

      c.x1 = Math.min( c.x1, n.positionX - n.width / 2 );
      c.x2 = Math.max( c.x2, n.positionX + n.width / 2 );
      c.y1 = Math.min( c.y1, n.positionY - n.height / 2 );
      c.y2 = Math.max( c.y2, n.positionY + n.height / 2 );
    }

    c.w = c.x2 - c.x1;
    c.h = c.y2 - c.y1;

    totalA += c.w * c.h;
  }

  components.sort( function( c1, c2 ){
    return c2.w * c2.h - c1.w * c1.h;
  } );

  var x = 0;
  var y = 0;
  var usedW = 0;
  var rowH = 0;
  var maxRowW = Math.sqrt( totalA ) * layoutInfo.clientWidth / layoutInfo.clientHeight;

  for( var i = 0; i < components.length; i++ ){
    var c = components[ i ];

    if( !c ){ continue; }

    for( var j = 0; j < c.length; j++ ){
      var n = c[ j ];

      if( !n.isLocked ){
        n.positionX += (x - c.x1);
        n.positionY += (y - c.y1);
      }
    }

    x += c.w + options.componentSpacing;
    usedW += c.w + options.componentSpacing;
    rowH = Math.max( rowH, c.h );

    if( usedW > maxRowW ){
      y += rowH + options.componentSpacing;
      x = 0;
      usedW = 0;
      rowH = 0;
    }
  }
};

export default CoseLayout;
